/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.dolphinscheduler.api.service.impl;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.dolphinscheduler.api.dto.MonitorDto;
import org.apache.dolphinscheduler.api.dto.WorkerZabbixDto;
import org.apache.dolphinscheduler.api.enums.Status;
import org.apache.dolphinscheduler.api.service.InterfaceService;
import org.apache.dolphinscheduler.api.service.MonitorService;
import org.apache.dolphinscheduler.api.service.WorkerGroupService;
import org.apache.dolphinscheduler.api.utils.PageInfo;
import org.apache.dolphinscheduler.api.vo.WorkerGroupGPUVO;
import org.apache.dolphinscheduler.common.constants.Constants;
import org.apache.dolphinscheduler.common.model.Server;
import org.apache.dolphinscheduler.common.model.WorkerServerModel;
import org.apache.dolphinscheduler.common.utils.JSONUtils;
import org.apache.dolphinscheduler.dao.MonitorDBDao;
import org.apache.dolphinscheduler.dao.dto.ZabbixDataDto;
import org.apache.dolphinscheduler.dao.entity.MonitorRecord;
import org.apache.dolphinscheduler.dao.entity.User;
import org.apache.dolphinscheduler.dao.entity.WorkerGroup;
import org.apache.dolphinscheduler.dao.mapper.WorkerGroupMapper;
import org.apache.dolphinscheduler.registry.api.RegistryClient;
import org.apache.dolphinscheduler.registry.api.enums.RegistryNodeType;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.text.NumberFormat;

import com.google.common.collect.Sets;

/**
 * monitor service impl
 */
@Service
@Slf4j
public class MonitorServiceImpl extends BaseServiceImpl implements MonitorService {

    @Autowired
    private MonitorDBDao monitorDBDao;

    @Autowired
    private RegistryClient registryClient;

    @Autowired
    @Lazy
    private WorkerGroupService workerGroupService;

    @Autowired
    private WorkerGroupMapper workerGroupMapper;
    /**
     * query database state
     *
     * @param loginUser login user
     * @return data base state
     */
    @Override
    public Map<String, Object> queryDatabaseState(User loginUser) {
        Map<String, Object> result = new HashMap<>();
        List<MonitorRecord> monitorRecordList = monitorDBDao.queryDatabaseState();
        result.put(Constants.DATA_LIST, monitorRecordList);
        putMsg(result, Status.SUCCESS);
        return result;
    }

    /**
     * query master list
     *
     * @param loginUser login user
     * @return master information list
     */
    @Override
    public Map<String, Object> queryMaster(User loginUser) {
        Map<String, Object> result = new HashMap<>();
        List<Server> masterServers = getServerListFromRegistry(true);
        result.put(Constants.DATA_LIST, masterServers);
        putMsg(result, Status.SUCCESS);

        return result;
    }

    /**
     * query worker list
     *
     * @param loginUser login user
     * @return worker information list
     */
    @Override
    public Map<String, Object> queryWorker(User loginUser) {

        Map<String, Object> result = new HashMap<>();
        List<WorkerServerModel> workerServers = getServerListFromRegistry(false)
                .stream()
                .map((Server server) -> {
                    WorkerServerModel model = new WorkerServerModel();
                    model.setId(server.getId());
                    model.setHost(server.getHost());
                    model.setPort(server.getPort());
                    model.setZkDirectories(Sets.newHashSet(server.getZkDirectory()));
                    model.setResInfo(server.getResInfo());
                    model.setCreateTime(server.getCreateTime());
                    model.setLastHeartbeatTime(server.getLastHeartbeatTime());
                    return model;
                })
                .collect(Collectors.toList());

        Map<String, WorkerServerModel> workerHostPortServerMapping = workerServers
                .stream()
                .collect(Collectors.toMap(
                        (WorkerServerModel worker) -> {
                            String[] s = worker.getZkDirectories().iterator().next().split("/");
                            return s[s.length - 1];
                        }, Function.identity(), (WorkerServerModel oldOne, WorkerServerModel newOne) -> {
                            oldOne.getZkDirectories().addAll(newOne.getZkDirectories());
                            return oldOne;
                        }));

        result.put(Constants.DATA_LIST, workerHostPortServerMapping.values());
        putMsg(result, Status.SUCCESS);

        return result;
    }

    @Override
    public List<Server> getServerListFromRegistry(boolean isMaster) {
        return isMaster
                ? registryClient.getServerList(RegistryNodeType.MASTER)
                : registryClient.getServerList(RegistryNodeType.WORKER);
    }

    @Override
    public Map<String, Object> queryWorkerByGroup(User loginUser) {
        Map<String, Object> result = new HashMap<>();
        List<WorkerServerModel> workerServers = getServerListFromRegistry(false)
                .stream()
                .map((Server server) -> {
                    WorkerServerModel model = new WorkerServerModel();
                    model.setId(server.getId());
                    model.setHost(server.getHost());
                    model.setPort(server.getPort());
                    model.setZkDirectories(Sets.newHashSet(server.getZkDirectory()));
                    model.setResInfo(server.getResInfo());
                    model.setCreateTime(server.getCreateTime());
                    model.setLastHeartbeatTime(server.getLastHeartbeatTime());
                    return model;
                })
                .collect(Collectors.toList());
        // ����worker��������з���
        Map<String, List<WorkerServerModel>> workerHostPortServerMapping = workerServers
                .stream()
                .collect(Collectors.groupingBy(
                        (WorkerServerModel worker) -> {
                            String[] s = worker.getZkDirectories().iterator().next().split("/");
                            return s[s.length - 2];
                        }));
        Map<String, MonitorDto> monitorMap = new HashMap<>();
        for (Map.Entry<String, List<WorkerServerModel>> stringListEntry : workerHostPortServerMapping.entrySet()) {
            MonitorDto monitorDto = new MonitorDto();
            monitorMap.put(stringListEntry.getKey(), monitorDto);
            List<MonitorDto> monitorDtoList = new ArrayList<>();
            for (WorkerServerModel workerServerModel : stringListEntry.getValue()) {
                MonitorDto dto = JSONUtils.parseObject(workerServerModel.getResInfo(), MonitorDto.class);
                monitorDtoList.add(dto);
            }
            monitorDto.setCpuUsage(BigDecimal.valueOf(monitorDtoList.stream().mapToDouble(MonitorDto::getCpuUsage).average().orElse(0d))
                    .setScale(2, RoundingMode.HALF_UP).doubleValue());
            monitorDto.setMemoryUsage(BigDecimal.valueOf(monitorDtoList.stream().mapToDouble(MonitorDto::getMemoryUsage).average().orElse(0d))
                    .setScale(2, RoundingMode.HALF_UP).doubleValue());
            monitorDto.setLoadAverage(BigDecimal.valueOf(monitorDtoList.stream().mapToDouble(MonitorDto::getLoadAverage).average().orElse(0d))
                    .setScale(2, RoundingMode.HALF_UP).doubleValue());
        }
        result.put(Constants.DATA_LIST, monitorMap);
        putMsg(result, Status.SUCCESS);
        return result;
    }

    @Override
    public Map<String, Object> queryWorkerResourceData(User loginUser, Integer pageNo, Integer pageSize, Integer groupId, Integer isVirtual) {
        // list from index
        int fromIndex = (pageNo - 1) * pageSize;
        // list to index
        int toIndex = (pageNo - 1) * pageSize + pageSize;

        int total = 0;
        List<WorkerZabbixDto> resultDataList = new ArrayList<>();


        Map<String, Object> result = new HashMap<>();
        List<WorkerZabbixDto> workerZabbixDtoList = Lists.newArrayList();
        // ��ѯ����worker�������
        Map<String, MonitorDto> workerMonitorData = workerGroupService.queryWorkerMonitorData();
        Map<String, MonitorDto> workerMonitorDataNew = new HashMap<>();
        List<String> ipList = Lists.newArrayList();
        for (Map.Entry<String, MonitorDto> stringMonitorDtoEntry : workerMonitorData.entrySet()) {
            workerMonitorDataNew.put(stringMonitorDtoEntry.getKey().split(":")[0], stringMonitorDtoEntry.getValue());
        }
        WorkerGroup workerGroup = null;
        if (Objects.nonNull(groupId)) {
            // ��ѯ����
            workerGroup = workerGroupService.getWorkerGroupById(groupId);
            // ��ȡ����������woker addr
            String addrList = workerGroup.getAddrList();
            List<String> addrs = Arrays.asList(addrList.split(","));
            ipList = addrs.stream().map(it -> it.split(":")[0]).collect(Collectors.toList());
        }else{
            ipList = new ArrayList<>(workerMonitorDataNew.keySet());
        }
        if (CollectionUtils.isNotEmpty(ipList)) {

            List<MonitorDto> workerGroupMonitorList = new ArrayList<>();
            for (String ip : ipList) {
                WorkerZabbixDto workerZabbixDto = new WorkerZabbixDto();
                // �������ṩ������
                MonitorDto monitorDto = workerMonitorDataNew.get(ip);
                workerZabbixDto.setIp(ip);
                if(Objects.nonNull(workerGroup)){
                    workerZabbixDto.setGroupName(workerGroup.getName());
                } else {
                	WorkerGroup wg = workerGroupService.queryWorkGroupByIp(ip);
                	if(wg != null) {
                		workerZabbixDto.setGroupName(wg.getName());
                	}
                }
                if (Objects.nonNull(monitorDto)) {
                    workerZabbixDto.setCreateTime(monitorDto.getCreateTime());
                    workerZabbixDto.setUsedMemory(monitorDto.getMemoryUsage());
                    workerZabbixDto.setServerStatus(monitorDto.getServerStatus());
                    workerZabbixDto.setCpuUsage(monitorDto.getCpuUsage());
                    workerZabbixDto.setPort(monitorDto.getPort());
                    workerZabbixDto.setCpus(monitorDto.getCpuNum());
                    workerZabbixDto.setMemory(monitorDto.getMemorySize());
                    workerZabbixDto.setArchitecture(monitorDto.getArchitecture());
                    workerZabbixDto.setName(monitorDto.getHostName());
                    workerZabbixDto.setSystemTotal(monitorDto.getSystemTotal());
                    workerZabbixDto.setSystemUsed(monitorDto.getSystemUsed());
                    workerZabbixDto.setDataTotal(monitorDto.getDataTotal());
                    workerZabbixDto.setDataUsed(monitorDto.getDataUsed());
                    workerZabbixDto.setIsVirtual(monitorDto.getIsVirtual());
                    workerZabbixDto.setGpuInfos(monitorDto.getGpuInfos());
                    workerGroupMonitorList.add(monitorDto);
                }

                if (Objects.nonNull(isVirtual)) {
                    if (isVirtual == workerZabbixDto.getIsVirtual()) {
                        workerZabbixDtoList.add(workerZabbixDto);
                    }
                } else {
                    workerZabbixDtoList.add(workerZabbixDto);
                }
            }
        }

        // ��ҳ
        total = workerZabbixDtoList.size();
        if (fromIndex < workerZabbixDtoList.size()) {
            if (toIndex > workerZabbixDtoList.size()) {
                toIndex = workerZabbixDtoList.size();
            }
            resultDataList = workerZabbixDtoList.subList(fromIndex, toIndex);
        }

        PageInfo<WorkerZabbixDto> pageInfo = new PageInfo<>(pageNo, pageSize);
        pageInfo.setTotal(total);
        pageInfo.setTotalList(resultDataList);

        result.put(Constants.DATA_LIST, pageInfo);
        putMsg(result, Status.SUCCESS);
        return result;
    }


    public List<ZabbixDataDto> queryZabbixData(List<String> ipList){
        List<ZabbixDataDto> zabbixDataDtoList = new ArrayList<>();
//        StringBuilder sb = new StringBuilder();
//        for (String ip : ipList) {
//            sb.append("ipList=");
//            sb.append(ip);
//            sb.append("&");
//        }
//        String result = HttpUtils.get("http://222.240.49.66:31480/zabbix/queryZabbixData?" + sb);
//
//        ObjectNode jsonNodes = org.apache.dolphinscheduler.common.utils.JSONUtils.parseObject(result);
//        JsonNode code = jsonNodes.get("code");
//        if(code.intValue() == 200){
//            JsonNode data = jsonNodes.get("data");
//            if(Objects.nonNull(data)){
//                zabbixDataDtoList = org.apache.dolphinscheduler.common.utils.JSONUtils.toList(jsonNodes.get("data").toString(), ZabbixDataDto.class);
//            }
//        }
        return zabbixDataDtoList;
    }

    @Override
    public Map<String, Object> queryGPUInfoFromCentral(User loginUser, int groupId) {
        // result --> Map<String, WorkerGroupGPUVO>
        Map<String, Object> result = new HashMap<>();
        result.put(String.valueOf(groupId), new WorkerGroupGPUVO());

        // query all worker info
        Map<String, Object> workersInfo =  queryWorker(loginUser);

        // get all worker info success?
        Status status = (Status) workersInfo.get(Constants.STATUS);
        if(status == Status.SUCCESS){

            // get worker list where worker group contains
            Set<String> addrSet = new HashSet<>();
            String addrListStr = getAddrListFromGroup(groupId);
            if(StringUtils.isNotBlank(addrListStr)){
                String[] addrStringArr = addrListStr.split(",");
                for(String addrString : addrStringArr){
                    addrSet.add(addrString.substring(0,addrString.indexOf(":")));
                }
            } else {
                result.put(Constants.STATUS,Status.ERROR);
                return result;
            }

            // get data from the workersInfo��Map��
            Collection<WorkerServerModel> data = (Collection<WorkerServerModel>) workersInfo.get(Constants.DATA_LIST);
            List<WorkerServerModel> list = new ArrayList<>();
            list.addAll(data);

            WorkerGroupGPUVO workerGroupGPUVO = (WorkerGroupGPUVO) result.get(String.valueOf(groupId));
            workerGroupGPUVO.setGroupId(groupId);

            int workerGpuNum = 0;
            double workerGpuUtil = 0.0;

            for (WorkerServerModel workerServerModel : list) {
            	String host = workerServerModel.getHost();
            	if(host.indexOf(":") > 0) {
            		host = host.substring(0, host.indexOf(":"));
            	}
                // specified group don't contains this worker
                if(!addrSet.contains(host)){
                    continue;
                }

                String resInfo = workerServerModel.getResInfo();
                MonitorDto dto = JSONUtils.parseObject(resInfo, MonitorDto.class);

                String gpuInfoStr = dto.getGpuInfos();
                // this worker don't have gpu
                if(StringUtils.isEmpty(gpuInfoStr) || gpuInfoStr.trim().equalsIgnoreCase("null")){
                    continue;
                }
                
                JSONArray gpuInfoArray = JSONObject.parseArray(gpuInfoStr);
                if(gpuInfoArray != null) {
                	 workerGpuNum += gpuInfoArray.size();

                     double gpuUtil = 0.0;
                     try {
                         gpuUtil = getGPUUtil(gpuInfoArray);
                     } catch (ParseException e) {
                         log.error("error:",e);
                     }

                     workerGpuUtil += gpuUtil;
                }
            }

            workerGroupGPUVO.setNum(workerGpuNum);
            workerGroupGPUVO.setGpuUitl(workerGpuNum==0?0.0:workerGpuUtil/workerGpuNum);
            result.put(Constants.STATUS, Status.SUCCESS);
            result.put(Constants.DATA_LIST, workerGroupGPUVO);
            return result;

        } else {
            result.put(Constants.STATUS,Status.ERROR);
            return result;
        }
    }

    public String getAddrListFromGroup(int groupId){
        WorkerGroup workerGroup = workerGroupMapper.selectById(groupId);
        if(workerGroup != null){
            return workerGroup.getAddrList();
        } else {
            return "";
        }
    }

    public double getGPUUtil(JSONArray gpuInfoArray) throws ParseException {
        if(gpuInfoArray == null || gpuInfoArray.size() == 0){
            return 0.0;
        }

        double gpuUtil = 0.0;

        NumberFormat nf = NumberFormat.getPercentInstance();
        for(int i=0; i<gpuInfoArray.size(); i++){
            JSONObject gpuInfoJsonObject = JSONObject.parseObject(gpuInfoArray.getString(i));
            gpuUtil += Double.parseDouble(gpuInfoJsonObject.getString("ops"));
        }
        return gpuUtil/gpuInfoArray.size();
    }
}
